from IPython.display import display, Javascript, HTML
display(HTML("<style>.jp-Notebook { width: 85% !important; }</style>"))
#### Hack för att bli av med ful-cellerna vid export till HTML ####
# Tagga med hide-cell, sen lägg till i den genererade html-filen i <style>:
# div.celltag_hide-cell {display: none;}
%%html
<style>
.jp-CodeCell .jp-Editor,
.jp-RenderedHTMLCommon {
font-size: 20px;
}
.jp-CodeCell .jp-InputArea-editor {
font-size: 20px;
}
</style>
Efter vårt första neuron-experiment, kan vi låtsas att vi efterliknar en laboration i kemi eller fysik där mätvärden på en $x-y$ graf ligger spridda över och under, men ca längs en linje. I normala fall hade läraren bett er sätta en linjal till pappret och dra ett streck sådant att det bäst representerar den ekvation som experimentet syftar till att ta reda på
Här har vi gjort så vi slipper titrera, mäta och hålla på, genom att ta "ekvationen som söks" och lagt till slumpfel uppåt och neråt (+/-) - i $facit(x)$ funktionen - så att $x-y$ grafen blir en brusig och yvig linje. Sen låtsas vi att dessa koordinater är era uppmätta värden som ni ska hitta bäst linje till. Lite baklänges sådär. Men nu har vi i a f en massa $x$ med tillhörande $y_{sann}$ (som alltså inte är så himla sanna) att mata neuronen med
Nu är det vår lilla NNs uppdrag att för varje givet $x$-värde ge förslag på ett $y$-värde. Sen får den reda på hur fel den hade i.f.h.t $y_{sann}$, vilket gör att vi med back-propagation kan justera $w$ och $b$ (jmfr $k$ och $m$ för räta linjen). När den sen blivit matad och tillrättavisad några hundra ggr, så tittar vi om inte både $w$ och $b$ har börjat likna $k$:et och $m$:et i $linje(x)$, den sanna ekvationen.
(Om man inte har nån linjal så får man hitta nåt annat sätt ju )
import random
import matplotlib.pyplot as plt
import numpy as np
import csv
# Så att det finns ett "minimum" att eftersträva
def abs_loss(y_sann, y_gissn):
return abs(y_sann - y_gissn)
def line(x):
return 10*x-5
def facit(x):
global rng
noise = rng.normal(0.0, 5)
return line(x) + noise
#samla statistic
Xs = []
Ys = []
y_line = []
rng = np.random.default_rng(12345)
def samlaStat(x,y):
global Xs, Ys
Xs.append(x)
Ys.append(y)
# Träningsdata förbereds
random.seed(42) # For reproducerbarhet när vi jämför varandras program
num_samples = 500 #så här många x-gissningar ska vi träna med
# Initialize weights and bias
w = random.uniform(-1,1)
b = random.uniform(-1,1)
# Träningsparametrar så den inte går för snabbt o flippar ut
learning_rate = 0.015
# Tränings loop
for num in range(num_samples):
x = random.uniform(-1,1) * 5
#y_sann = 2 * x +3 # f(x) = 3*x + 2
y_sann = facit(x)
# Framåt
Zum = x * w + b # Slutliga gissningen, som ska jmfrs med facit (y_sann)
# Beräkna fel / loss
loss = abs_loss(y_sann, Zum)
# Baklänges pass - beräkna den totala gradienten / lutningen m.h.a kedjeregeln
dLoss_dyGiss = 1 if Zum > y_sann else -1
dLoss_dw = dLoss_dyGiss * x #(jmfr y = kx + m)
dLoss_db = dLoss_dyGiss # * 1 egentligen
# Uppdatera weights & bias
w -= learning_rate * dLoss_dw
b -= learning_rate * dLoss_db
if num % 10 == 0: # Var tionde
#print(f"ant_x {num}, Loss: {loss:.4f}")
samlaStat(x, y_sann)
y_line.append(line(x))
# Detta ska jämföras med facit-funktionen f(x) = (def line(x) ovan)
print(len(Xs))
print(f"Färdigtränad weight: {w}")
print(f"tränad bias: {b}")
if True:
plt.scatter(Xs, Ys)
plt.plot(Xs, y_line, color='red')
plt.show()
# Skriv data till fil
with open('matdata.csv', 'w', newline='') as file:
writer = csv.writer(file, delimiter=' ')
writer.writerow(Xs)
writer.writerow(Ys)
# testa på
# https://stats.blue/Stats_Suite/correlation_regression_calculator.html
#
50 Färdigtränad weight: 10.0344334112758 tränad bias: -3.019978489554668
Så här var ambitionen. Att när vi väl sett att det gick att känna igen de olika "bilderna", så visste vi i.a.f. att det var möjligt. Nu är frågan om vi kan få nätverket till att träna sig så att det klarar uppgiften. Inte (alls) nödvändigtvis med samma metod, i slutändan. Men med någon, åtminstone
Här visade det sig att man faktiskt kunde få nätverket att kategorisera typer av input, ifall man kopplade på just detta sätt i.a.f. Så man hade visat "existens", men inte metod för att komma dit.
Detta (nedan bild), är ett enklare nätverk och klarar kanske jobbet nästan lika bra(?)
Istället för att resonera fram till hur man skulle kunna använda wikterna och bias för att trolla fram rätt förslag såsom i videon, låter vi nätverket träna sig själv med hjälp av en Loss funktion som tar det genomsnittliga kvadratavståndet, Mean Square Error (MSE) mellan prediktion och rätt svar.
I förra övningen använde vi ju $L(Z)=abs(Z-Y_{sann})$, alltså absolutbeloppet av skillnaden, också en funktion med ett väldefinierat minimum, men här går vi över till MSE eftersom i en andragradare typ ($x^2$) finns inga svaga punkter, och är deriverbar (hitta k-värdet) även om gissningen skulle visa sig vara rakt på det rätta svaret ($Y_{sann}$). och är en vippebräda, och omöjlig att hitts k-värde till. Dessutom är lutningen på en mjuk Loss-funktionen en indikation på hur långt vi är från minimi-punkten. Kom ihåg att även om vi kommer få värdet, och därmed hur fel, finns det inget lätt sätt att beräkna hur mycket vi hade skullat ändra våra vikter för att eliminera det.
Med ett uttryck som MSE vet vi åtminstone vilken sida om det rätta svaret vi befinner oss på. Lutningen (eller k-värdet) är större ju mer fel gissningen är när vi har en andragradere som Loss-funktion. Detta faktum används också ofta till att bestämma med vilken grad man stegar upp iterationerna i datorkod (vi har inte gjort denna anpassning vare sig i förra eller ens denna uppgiften. Eftersom datorn inte lider så värst utan sådan optimering i denna uppgift.
Den grå cirkeln på det mittersta lagret i ritningen ovan är vår aktiveringsfunktion. Den, som ni säkert har hört tusen ggr vid detta lag, är HELA anledingen till att ett neuralt nätverk kan lära sig katgorisera mellan fler saker än de (två nödvändigtvis) som kan skiljas åt med en rät linje (som vi gjorde i förra övninen. Antingen var punkten över, eller så var den under "ekvationen". Sambanden kan vara hur invecklade och komplexa som helst. Det är bara att fläska på med fler lager och fler neuroner och mer träning. Och fler datorer då förstås.
Innan man börjar måste vi komma på ett sätt att skicka in datan i nätverket samt ett sätt att representera rätt svar på. Ja/nej eller hur nära (mer om det längre ner). $X$-input får bli fyra tal, ett för varje pixel, med värden mellan -1 och 1, helsvart till helvitt, samlade i en vektor, avlästa från "bilden" vänster till höger, uppifrån och ner.
# Här är alla åtta mönster med tillhörande svar. Men vi tar enstaka, till att börja med iaf.
Xalla = np.array([[1, -1, 1, -1], [-1, -1, 1, 1], [-1, 1, 1, -1], [-1, -1, -1, -1],
[-1, 1, -1, 1], [1, 1, -1, -1], [1, -1, -1, 1], [1, 1, 1, 1]])
# Vert Hor Diag Solid
Yalla = np.array([[0, 0, 0, 1], [0, 0, 1, 0], [0, 1, 0, 0], [1, 0, 0, 0],
[0, 0, 0, 1], [0, 0, 1, 0], [0, 1, 0, 0], [1, 0, 0, 0]])
Nedan är resultatet, från ett program jag redan skrivit, när jag testar efter ha tränat med alla ovanstående input ($X$) och svar ($Y$). Testet var att identifiera de fyra första inputen (eller fyra sista, som ju är samma kategorier) från $X_{alla}$, dvs (solid, diag, hor, vert). Och det kom ut ganska hyggligt, med en av siffrorna i varje svars-rad nära $1$ och de andra, nära $0$. Tillräckligt nära, i alla fall, för att man ska kunna sluta sig till vilket mönster det var frågan om
(Det gav ett rätt hyggligt resultat ... typ )
Vi sätter igång, och inför numpy, ett Python-paket som gör det enkelt att räkna med vektorer och matriser (många siffror på en gång), som om de vore enkla skalärer.
!pip install numpy
Vi parameteriserar (hittar på namn för konstanter) från början hur stort varje lager (tre i allt) skall vara, så att vidare kod bygger på dessa istället för hårda siffror. Så ifall vi skulle vilja ändra senare behöver vi bara ändra värden där dessa sätts i början, på ett ställe i koden, istället för att jaga siffror överallt. Trust me, det blir bättre så ... om än något styltigare.
# Hur många neuroner som varje lager ska ha
input_size = 4
hidden_size = 6
output_size = 4
import numpy as np #Kompisen numpy
# Vikterna slumpas mellan -1 och 1
W1 = np.random.randn(input_size, hidden_size)
W2 = np.random.randn(hidden_size, output_size)
# Låt bias vara noll första ggn. Dom kommer ändå vara med i uppdateringen, men ...
# ... matten blir mindre klottrig på så sätt
b1 = np.zeros(hidden_size)
b2 = np.zeros(output_size)
W1 # oups, det här skulle vi kanske inte ha visat riktigt än
array([[ 8.10227756e-01, -1.03213725e+00, 1.30103711e+00,
-1.39153195e+00, -1.11178880e-01, -1.02164994e+00],
[-1.50392146e+00, -1.15548554e+00, -1.12855887e+00,
-2.51622137e-01, -1.34301964e-03, -1.00638873e+00],
[ 2.86858371e-01, -2.53180313e-01, 1.53217355e+00,
-2.37793748e-01, -2.59928649e-02, 5.61227162e-01],
[ 6.16974641e-01, -1.03494413e+00, -7.28138176e-02,
-2.10759152e-01, 8.04421712e-01, -1.25382244e-01]])
X = np.array([1, -1, -1, 1]) #diagonal
# I första omgången är ju "b":na alla nollor, så vi tar inte med dom
# ... i beräkningen för vårt första Z1
Z1=np.dot(X,W1)
Z1
array([ 0.15395423, 2.34850344, 0.60589301, 0.46569974, -0.22227896,
5.00057989])
Vi behöver också definiera vår enda aktiveringsfunktion $tanh(z)$. Och den är redan färdig i Numpy ...
... så vi behöver bara skriva:
def tanh(x):
return np.tanh(x)
... Och så gör den det för alla sex värden i Z1, så här
A1=tanh(Z1)
A1
array([ 0.96288196, 0.71877794, 0.99937411, -0.82329015, -0.73533729,
0.58586602])
Och sen bara en 'repeat' av vad vi gjorde för att beräkna $Z1$. Skalärprodukt igen
# Skippar bias här också, men dom kommer med i leken när vi uppdaterar
Z2=np.dot(A1,W2)
Z2
array([-2.46248447, -2.33512413, 0.2132289 , 0.55320145])
Jag har bestämt oss för att använda MSE, dvs en Loss-funktion som tar differansen mellan vår gissning $Z_2$ och $Y_{sann}$, kvadrerar det, och tar genomsnittet (delar med 4 alltså)
#Och låt oss inte glömma Y_sann, som får reprensentera någon av de fyra mönstren
Y = np.array([0, 0, 0, 1])
Så här skulle man kunna implementera MSE ...
# Z2 = [-2.39814425, -2.18459557, -0.84819751, 3.14512432]
diff = Z2-Y
sum = 0
for i in diff:
sum += i**2
L_mse = sum/4.0
print(L_mse)
3.961137748796248
Men den finns också inbyggd i numpy, så ..
# Vi ska se om numpy fixar detta också, så om vi definierar
# MSE loss function
def loss_mse(output, Y):
diff = output - Y
loss = np.mean(diff**2)
return loss
# Så borde vi få samma
print(loss_function(Z2,Y))
3.961137748796248
På detta sätt bildas ju två stycken tvådimensionella vanliga kurvor, som båda har väldefinierade tangenter, med lutningarna (k-värdena) $k_{x_1}=dL/dx_1$ och $k_{x_2}=dL/dx_2$. Om man sätter ihop dessa tangenter till en vektor (som kallas 'gradient' ($\nabla$), uttalas nabla) kommer den att peka åt det hållet där 3D-ytan ökar snabbast (återigen universitetsmatte, men kanske också ganska intuitivt om man känner efter). Vi vill därför gå åt motsatta hållet, $-\nabla$
from IPython.display import Video
Video("/Users/ulfschack/Lektioner/prog/jupyter_env/JPNfiler/teknik1/NeuralNet/Neural_full/_site/slide_4PixelNN/croppat.mp4", embed=True, html_attributes="controls loop")
För tillfället ska ni betrakta nätverket som skalärt. Dvs ett singulärt värde för $X$, $b1$, $w2$ ... etc
Påminner om varför vi använder kedjeregeln.
from IPython.display import Video
Video("/Users/ulfschack/Lektioner/prog/jupyter_env/JPNfiler/teknik1/NeuralNet/Neural_full/_site/slide_4PixelNN/chainRule.mp4", embed=True, html_attributes="controls loop")
Det finns en anledning till att färgkoda x- och y-axlarna som utgör trianglar för beräkning av k-värdet. Den vänstrastes y-värde är den mellerstas x-värde, som till slut är ingående x-värde till den högra. Varefter även den funktionen gör vad den vill med det (i detta fall bara gångar med 3, som en tråkmåns). Det samlade k-värdet blir således $2*0.5*3=3$, efter multiplikation
Så här multipliceras matriser. Jag tar upp det, eftersom det visar sig att ifall man vill gånga varje X-värde med sina respektive vikter på väg in mot en neuron, så blir matten precis densamma, ifall man vänder grejer rätt.
Räkna exempel-matriser och leverera dw1, db1, dw2, och db2. Du kan använda papper och penna, Python, Geogebra, eller vad du vill. Bara du lär dig, och kan redovisa det.
Fast du vill nog läsa repetitionen nedan, en gång eller två.
Nedan, ska föreställa en ritning på nätverket i något större detalj. Jag har valt ut två neuroner. En från det första lagret som är direkt kopplat till input ($\vec{X}$, och en som från det sista. Här ser man hur summan i neuroner bildas och vilka och hur många vikter som är inblandade. Jag hoppas att ritningen kan hjälpa en att fundera igenom hur långa/stora vektorerena måste vara för att passa varandra.
T.ex var i matrisen $W1$ sitter - och vilket index har - den vikt som skall multilpliceras med, säg, $x_1$ och som till slut bli del i bl a $z_4$, strax före aktiveringsfunktionen osv. Låter det komplicerat? - Det var meningen, och lite överdrivet såklart, men sånt måste tänkas igenom åtminstone en gång. Sen ser man hela matriser och vektorer som vilka variabler som helst. Dessutom bruka programmet crascha om man vänt något fel ;).
En av anledningarna till att jag valde 4x6x4 för nätverket var just att det skulle bli tydligare hur matriser vänds och vrids. Det hade man ju inte lätt kunnat se i det ursprungliga 4x4x4 nätverket, där allt har samma dimension.
... och vi struntar fortfarande i $b-värdena$, som du ser.
Repris från ovan, så gäller rent allmänt alltså:
En ytterligare överrasking, om ni är redo, är att ifall man ställer vektorer alldeles "fel", båda två uppochner, så får man faktiskt också ett resultat. Produkten blir med ens multidimensionell och varje element i vektorn multipliceras bara en gång med motsvarande i den andra. Så här (se bild)
Ok, vidare med uppgiften ...
$k$-värdet för loss-funktionen är samma som för den kurva vi räknade på tavlan med $\frac{\Delta y}{\Delta x}$ för $y=x^2$ (när triangelns sidor gick mot noll), nämligen $2 \cdot x$. Ersätt bara $x$ med $(Z_2-Y_{sann})$ så får man: $k_{loss}=2 \cdot (Z_2 - Y_{sann})$. Och på Pythonesiska blir det:
dLdZ_2 = 2 * (Z2 - Y) # MSE back-derivative fram till sista Z:at
dLdZ_2
array([-4.92496893, -4.67024825, 0.4264578 , -0.8935971 ])
# En 6x1-vektor "gångat med" en 1x4-vektor ger en 6x4-matris, ...
# ... vilket ju är precis vad vi behöver
dW2 = np.outer(A1.T, dLdZ_2)
# Numpy använder tyvärr en annan funktion, 'np.outer' (istf. np.dot) när båda är vektorer.
# (För att vända en rad-vektor till en kolumn-vector används ".T" (T för 'tranponering'))
dW2
array([[-4.74216373, -4.49689778, 0.41062853, -0.86042853],
[-3.53995904, -3.35687144, 0.30652846, -0.64229789],
[-4.92188643, -4.66732518, 0.42619089, -0.89303781],
[ 4.05467842, 3.8449694 , -0.35109851, 0.73568969],
[ 3.62151333, 3.43420771, -0.31359033, 0.65709528],
[-2.88537197, -2.73613978, 0.24984714, -0.52352818]])
# Eftersom Z2 = W2*A1 + b, så är lutningen (m.a.p 'b') lika med 1, bara. Alltså är
db2 = dLdZ_2 * 1
# Dvs samma
Med hjälp av detta bör du själv kunna beräkna $dw_1$ och $db_1$. Jag påminner om att du fått (se slides om det inte låter bekant) $k$-värdet för aktiveringsfunktionen av mig helt gratis, nämligen att $k_{tanh}=1 - tanh(Z_2)^2$ ... eftersom du inte har hunnit plugga matten bakom, än
Precis som att du tycker det är naturligt att en linje mellan två punkter på en kurva (sekant, heter det), har en lutning, så finns det även en lutning i det fallet de två punkterna har (nästan) samma x-värde, smälter ihop liksom. Och då kallas den för tangent. Newton Och Leibniz bevisade detta för mer än 300 år sen, och du kommer läsa mer om det under följande terminer. För att skingra eventuell förvirring vill jag understryka att om funktionen $L(z)$ har en tangent, vid nåt $x$-värde, och denna tangent har en viss lutning t.ex $k_{dL/db1}=3$, så växer $dL$ med 3 för varje gång dZ växer med 1. Gå inte vidare ifall du inte tycker detta självklart.
EN gång till: Om $k=\frac{dL}{dZ_2}=3$ så är $dL=3 \cdot dZ_2$ vilket är samma som $dZ_2=\frac{dL}{3}$ $\small \text{(Jämför med t.ex }\frac{4}{x}=3\Leftrightarrow x = \frac{4}{3}$).
Bedömning: Ge mig:
Och så till slut det där 'programmet' jag pratade om att ha skrivit, i början:
Jag har i.o.f.s slagit ihop alla åtta möjliga (om vi pratar igenkänneliga mönster, annars 16) kombinationer som input $X$ (och dess motsvarande $Y_{sann}$-värden), och låtit datorn snurra på
import numpy as np
np.random.seed(42)
# Hyperparameters
num_epochs = 600
learning_rate = 0.015
# Aktiveringsfunktion
def tanh(x):
return np.tanh(x)
# Neural network arkitektur
input_size = 4
hidden_size = 6
output_size = 4
# Initialisera vikter o bias
W1 = np.random.randn(input_size, hidden_size)
b1 = np.zeros(hidden_size)
W2 = np.random.randn(hidden_size, output_size)
b2 = np.zeros(output_size)
# Forward propagation - framåt
def forward_simple(X):
Z1 = np.dot(X, W1) + b1
A1 = tanh(Z1)
#A1 = relu(Z1)
Z2 = np.dot(A1, W2) + b2
#output = Z2
return A1, Z2
# MSE loss function
def loss_function(output, Y):
diff = output - Y
loss = np.mean(diff**2)
return loss
# Backward propagation - bakåt
def backward_simple(X, Y, A1, output):
delta2 = 2 * (output - Y) # MSE back-derivative
dW2 = np.dot(A1.T, delta2)
db2 = np.sum(delta2, axis=0)
delta1 = np.dot(delta2, W2.T) * (1 - A1**2) #lutn tanh (1- tan(z)^2)
#delta1 = np.dot((1-A1**2), W2) * delta2
dW1 = np.dot(X.T, delta1)
db1 = np.sum(delta1, axis=0)
return dW1, db1, dW2, db2
# Sample data
X = np.array([[1, -1, 1, -1], [-1, -1, 1, 1], [-1, 1, 1, -1], [-1, -1, -1, -1],
[-1, 1, -1, 1], [1, 1, -1, -1], [1, -1, -1, 1], [1, 1, 1, 1]])
# Vert Hor Diag Solid
Y = np.array([[0, 0, 0, 1], [0, 0, 1, 0], [0, 1, 0, 0], [1, 0, 0, 0],
[0, 0, 0, 1], [0, 0, 1, 0], [0, 1, 0, 0], [1, 0, 0, 0]])
# Träning
for epoch in range(num_epochs):
A1, output = forward_simple(X)
loss = loss_function(output, Y)
dW1, db1, dW2, db2 = backward_simple(X, Y, A1, output)
W1 -= learning_rate * dW1
b1 -= learning_rate * db1
W2 -= learning_rate * dW2
b2 -= learning_rate * db2
if epoch % 100 == 0:
print(f"Epoch {epoch}, Loss: {loss:.4f}")
# Test
# Solid Diad Hor Vert
test_data = np.array([[1, 1, 1, 1], [-1, 1, 1, -1], [1, 1, -1, -1], [-1, 1, -1, 1]])
A1, output = forward_simple(test_data)
print("Test data:")
print(test_data)
print("Output:")
print(output)
Epoch 0, Loss: 4.0068 Epoch 100, Loss: 0.0478 Epoch 200, Loss: 0.0115 Epoch 300, Loss: 0.0042 Epoch 400, Loss: 0.0017 Epoch 500, Loss: 0.0006 Test data: [[ 1 1 1 1] [-1 1 1 -1] [ 1 1 -1 -1] [-1 1 -1 1]] Output: [[ 9.82867583e-01 2.79903366e-03 -1.32253004e-04 -4.35406409e-03] [ 1.44152222e-02 9.98089534e-01 4.01300918e-03 8.05177295e-03] [ 1.38854689e-02 -9.52307649e-04 9.96593435e-01 -1.73793410e-03] [-5.27810480e-02 1.19446180e-02 1.33866914e-02 1.00146123e+00]]
... som jag sa i början. Dessa mönster känns igen. Och inget vi berättade, eller avslöjade i koden på särskilt vis, avslöjade vad vi var ute efter.
Tänk hur långt det kan gå, och vilken nytta man kan ha av sån här allmängiltig design. Nästan som om det är slutet för programmerar-yrket, fast nån måste ju hålla koll på maskinerna, så varför inte du ;). Men i.a.f för vår del här och nu. Trevlig sommar :)